﻿/**
 * Outlayer Item
 */

(function (window) {

    'use strict';

    // ----- get style ----- //

    var getComputedStyle = window.getComputedStyle;
    var getStyle = getComputedStyle ?
      function (elem) {
          return getComputedStyle(elem, null);
      } :
      function (elem) {
          return elem.currentStyle;
      };


    // extend objects
    function extend(a, b) {
        for (var prop in b) {
            a[prop] = b[prop];
        }
        return a;
    }

    function isEmptyObj(obj) {
        for (var prop in obj) {
            return false;
        }
        prop = null;
        return true;
    }

    // http://jamesroberts.name/blog/2010/02/22/string-functions-for-javascript-trim-to-camel-case-to-dashed-and-to-underscore/
    function toDash(str) {
        return str.replace(/([A-Z])/g, function ($1) {
            return '-' + $1.toLowerCase();
        });
    }

    // -------------------------- Outlayer definition -------------------------- //

    function outlayerItemDefinition(EventEmitter, getSize, getStyleProperty) {

        // -------------------------- CSS3 support -------------------------- //

        var transitionProperty = getStyleProperty('transition');
        var transformProperty = getStyleProperty('transform');
        var supportsCSS3 = transitionProperty && transformProperty;
        var is3d = !!getStyleProperty('perspective');

        var transitionEndEvent = {
            WebkitTransition: 'webkitTransitionEnd',
            MozTransition: 'transitionend',
            OTransition: 'otransitionend',
            transition: 'transitionend'
        }[transitionProperty];

        // properties that could have vendor prefix
        var prefixableProperties = [
          'transform',
          'transition',
          'transitionDuration',
          'transitionProperty'
        ];

        // cache all vendor properties
        var vendorProperties = (function () {
            var cache = {};
            for (var i = 0, len = prefixableProperties.length; i < len; i++) {
                var prop = prefixableProperties[i];
                var supportedProp = getStyleProperty(prop);
                if (supportedProp && supportedProp !== prop) {
                    cache[prop] = supportedProp;
                }
            }
            return cache;
        })();

        // -------------------------- Item -------------------------- //

        function Item(element, layout) {
            if (!element) {
                return;
            }

            this.element = element;
            // parent layout class, i.e. Masonry, Isotope, or Packery
            this.layout = layout;
            this.position = {
                x: 0,
                y: 0
            };

            this._create();
        }

        // inherit EventEmitter
        extend(Item.prototype, EventEmitter.prototype);

        Item.prototype._create = function () {
            // transition objects
            this._transn = {
                ingProperties: {},
                clean: {},
                onEnd: {}
            };

            this.css({
                position: 'absolute'
            });
        };

        // trigger specified handler for event type
        Item.prototype.handleEvent = function (event) {
            var method = 'on' + event.type;
            if (this[method]) {
                this[method](event);
            }
        };

        Item.prototype.getSize = function () {
            this.size = getSize(this.element);
        };

        /**
         * apply CSS styles to element
         * @param {Object} style
         */
        Item.prototype.css = function (style) {
            var elemStyle = this.element.style;

            for (var prop in style) {
                // use vendor property if available
                var supportedProp = vendorProperties[prop] || prop;
                elemStyle[supportedProp] = style[prop];
            }
        };

        // measure position, and sets it
        Item.prototype.getPosition = function () {
            var style = getStyle(this.element);
            var layoutOptions = this.layout.options;
            var isOriginLeft = layoutOptions.isOriginLeft;
            var isOriginTop = layoutOptions.isOriginTop;
            var x = parseInt(style[isOriginLeft ? 'left' : 'right'], 10);
            var y = parseInt(style[isOriginTop ? 'top' : 'bottom'], 10);

            // clean up 'auto' or other non-integer values
            x = isNaN(x) ? 0 : x;
            y = isNaN(y) ? 0 : y;
            // remove padding from measurement
            var layoutSize = this.layout.size;
            x -= isOriginLeft ? layoutSize.paddingLeft : layoutSize.paddingRight;
            y -= isOriginTop ? layoutSize.paddingTop : layoutSize.paddingBottom;

            this.position.x = x;
            this.position.y = y;
        };

        // set settled position, apply padding
        Item.prototype.layoutPosition = function () {
            var layoutSize = this.layout.size;
            var layoutOptions = this.layout.options;
            var style = {};

            if (layoutOptions.isOriginLeft) {
                style.left = (this.position.x + layoutSize.paddingLeft) + 'px';
                // reset other property
                style.right = '';
            } else {
                style.right = (this.position.x + layoutSize.paddingRight) + 'px';
                style.left = '';
            }

            if (layoutOptions.isOriginTop) {
                style.top = (this.position.y + layoutSize.paddingTop) + 'px';
                style.bottom = '';
            } else {
                style.bottom = (this.position.y + layoutSize.paddingBottom) + 'px';
                style.top = '';
            }

            this.css(style);
            this.emitEvent('layout', [this]);
        };


        // transform translate function
        var translate = is3d ?
          function (x, y) {
              return 'translate3d(' + x + 'px, ' + y + 'px, 0)';
          } :
          function (x, y) {
              return 'translate(' + x + 'px, ' + y + 'px)';
          };


        Item.prototype._transitionTo = function (x, y) {
            this.getPosition();
            // get current x & y from top/left
            var curX = this.position.x;
            var curY = this.position.y;

            var compareX = parseInt(x, 10);
            var compareY = parseInt(y, 10);
            var didNotMove = compareX === this.position.x && compareY === this.position.y;

            // save end position
            this.setPosition(x, y);

            // if did not move and not transitioning, just go to layout
            if (didNotMove && !this.isTransitioning) {
                this.layoutPosition();
                return;
            }

            var transX = x - curX;
            var transY = y - curY;
            var transitionStyle = {};
            // flip cooridinates if origin on right or bottom
            var layoutOptions = this.layout.options;
            transX = layoutOptions.isOriginLeft ? transX : -transX;
            transY = layoutOptions.isOriginTop ? transY : -transY;
            transitionStyle.transform = translate(transX, transY);

            this.transition({
                to: transitionStyle,
                onTransitionEnd: {
                    transform: this.layoutPosition
                },
                isCleaning: true
            });
        };

        // non transition + transform support
        Item.prototype.goTo = function (x, y) {
            this.setPosition(x, y);
            this.layoutPosition();
        };

        // use transition and transforms if supported
        Item.prototype.moveTo = supportsCSS3 ?
          Item.prototype._transitionTo : Item.prototype.goTo;

        Item.prototype.setPosition = function (x, y) {
            this.position.x = parseInt(x, 10);
            this.position.y = parseInt(y, 10);
        };

        // ----- transition ----- //

        /**
         * @param {Object} style - CSS
         * @param {Function} onTransitionEnd
         */

        // non transition, just trigger callback
        Item.prototype._nonTransition = function (args) {
            this.css(args.to);
            if (args.isCleaning) {
                this._removeStyles(args.to);
            }
            for (var prop in args.onTransitionEnd) {
                args.onTransitionEnd[prop].call(this);
            }
        };

        /**
         * proper transition
         * @param {Object} args - arguments
         *   @param {Object} to - style to transition to
         *   @param {Object} from - style to start transition from
         *   @param {Boolean} isCleaning - removes transition styles after transition
         *   @param {Function} onTransitionEnd - callback
         */
        Item.prototype._transition = function (args) {
            // redirect to nonTransition if no transition duration
            if (!parseFloat(this.layout.options.transitionDuration)) {
                this._nonTransition(args);
                return;
            }

            var _transition = this._transn;
            // keep track of onTransitionEnd callback by css property
            for (var prop in args.onTransitionEnd) {
                _transition.onEnd[prop] = args.onTransitionEnd[prop];
            }
            // keep track of properties that are transitioning
            for (prop in args.to) {
                _transition.ingProperties[prop] = true;
                // keep track of properties to clean up when transition is done
                if (args.isCleaning) {
                    _transition.clean[prop] = true;
                }
            }

            // set from styles
            if (args.from) {
                this.css(args.from);
                // force redraw. http://blog.alexmaccaw.com/css-transitions
                var h = this.element.offsetHeight;
                // hack for JSHint to hush about unused var
                h = null;
            }
            // enable transition
            this.enableTransition(args.to);
            // set styles that are transitioning
            this.css(args.to);

            this.isTransitioning = true;

        };

        var itemTransitionProperties = transformProperty && (toDash(transformProperty) +
          ',opacity');

        Item.prototype.enableTransition = function (/* style */) {
            // only enable if not already transitioning
            // bug in IE10 were re-setting transition style will prevent
            // transitionend event from triggering
            if (this.isTransitioning) {
                return;
            }

            // make transition: foo, bar, baz from style object
            // TODO uncomment this bit when IE10 bug is resolved
            // var transitionValue = [];
            // for ( var prop in style ) {
            //   // dash-ify camelCased properties like WebkitTransition
            //   transitionValue.push( toDash( prop ) );
            // }
            // enable transition styles
            // HACK always enable transform,opacity for IE10
            this.css({
                transitionProperty: itemTransitionProperties,
                transitionDuration: this.layout.options.transitionDuration
            });
            // listen for transition end event
            this.element.addEventListener(transitionEndEvent, this, false);
        };

        Item.prototype.transition = Item.prototype[transitionProperty ? '_transition' : '_nonTransition'];

        // ----- events ----- //

        Item.prototype.onwebkitTransitionEnd = function (event) {
            this.ontransitionend(event);
        };

        Item.prototype.onotransitionend = function (event) {
            this.ontransitionend(event);
        };

        // properties that I munge to make my life easier
        var dashedVendorProperties = {
            '-webkit-transform': 'transform',
            '-moz-transform': 'transform',
            '-o-transform': 'transform'
        };

        Item.prototype.ontransitionend = function (event) {
            // disregard bubbled events from children
            if (event.target !== this.element) {
                return;
            }
            var _transition = this._transn;
            // get property name of transitioned property, convert to prefix-free
            var propertyName = dashedVendorProperties[event.propertyName] || event.propertyName;

            // remove property that has completed transitioning
            delete _transition.ingProperties[propertyName];
            // check if any properties are still transitioning
            if (isEmptyObj(_transition.ingProperties)) {
                // all properties have completed transitioning
                this.disableTransition();
            }
            // clean style
            if (propertyName in _transition.clean) {
                // clean up style
                this.element.style[event.propertyName] = '';
                delete _transition.clean[propertyName];
            }
            // trigger onTransitionEnd callback
            if (propertyName in _transition.onEnd) {
                var onTransitionEnd = _transition.onEnd[propertyName];
                onTransitionEnd.call(this);
                delete _transition.onEnd[propertyName];
            }

            this.emitEvent('transitionEnd', [this]);
        };

        Item.prototype.disableTransition = function () {
            this.removeTransitionStyles();
            this.element.removeEventListener(transitionEndEvent, this, false);
            this.isTransitioning = false;
        };

        /**
         * removes style property from element
         * @param {Object} style
        **/
        Item.prototype._removeStyles = function (style) {
            // clean up transition styles
            var cleanStyle = {};
            for (var prop in style) {
                cleanStyle[prop] = '';
            }
            this.css(cleanStyle);
        };

        var cleanTransitionStyle = {
            transitionProperty: '',
            transitionDuration: ''
        };

        Item.prototype.removeTransitionStyles = function () {
            // remove transition
            this.css(cleanTransitionStyle);
        };

        // ----- show/hide/remove ----- //

        // remove element from DOM
        Item.prototype.removeElem = function () {
            this.element.parentNode.removeChild(this.element);
            this.emitEvent('remove', [this]);
        };

        Item.prototype.remove = function () {
            // just remove element if no transition support or no transition
            if (!transitionProperty || !parseFloat(this.layout.options.transitionDuration)) {
                this.removeElem();
                return;
            }

            // start transition
            var _this = this;
            this.on('transitionEnd', function () {
                _this.removeElem();
                return true; // bind once
            });
            this.hide();
        };

        Item.prototype.reveal = function () {
            delete this.isHidden;
            // remove display: none
            this.css({ display: '' });

            var options = this.layout.options;
            this.transition({
                from: options.hiddenStyle,
                to: options.visibleStyle,
                isCleaning: true
            });
        };

        Item.prototype.hide = function () {
            // set flag
            this.isHidden = true;
            // remove display: none
            this.css({ display: '' });

            var options = this.layout.options;
            this.transition({
                from: options.visibleStyle,
                to: options.hiddenStyle,
                // keep hidden stuff hidden
                isCleaning: true,
                onTransitionEnd: {
                    opacity: function () {
                        // check if still hidden
                        // during transition, item may have been un-hidden
                        if (this.isHidden) {
                            this.css({ display: 'none' });
                        }
                    }
                }
            });
        };

        Item.prototype.destroy = function () {
            this.css({
                position: '',
                left: '',
                right: '',
                top: '',
                bottom: '',
                transition: '',
                transform: ''
            });
        };

        return Item;

    }

    // -------------------------- transport -------------------------- //

    if (typeof define === 'function' && define.amd) {
        // AMD
        define([
            'eventEmitter/EventEmitter',
            'get-size/get-size',
            'get-style-property/get-style-property'
        ],
          outlayerItemDefinition);
    } else if (typeof exports === 'object') {
        // CommonJS
        module.exports = outlayerItemDefinition(
          require('wolfy87-eventemitter'),
          require('get-size'),
          require('desandro-get-style-property')
        );
    } else {
        // browser global
        window.Outlayer = {};
        window.Outlayer.Item = outlayerItemDefinition(
          window.EventEmitter,
          window.getSize,
          window.getStyleProperty
        );
    }

})(window);
